Conversation
* feat(UserProfile): build screen UserProfile # Conflicts: # src/lib/features/main/view/screens/main_screen.dart * feat: switch dark/light theme * fix: black color theme * fix: black theme in statistics screen * feat: add dark theme to auth screen * feat: apply dark theme for bottom navigation bar
…tion version 1 (#31) * feat(task): implement priority and tag selection features in task creation * Update README.md * Update README.md * Update README.md --------- Co-authored-by: Tran Quang Ha <[email protected]>
Removed commented section about main UI.
* feat(task): implement priority and tag selection features in task creation * feat(tags): enhance tag management with custom tag creation and selection * Update README.md * Update README.md
* feat(core): add auth layout template, custom textfield and colors * feat(auth): implement viewmodels for auth flow (MVVM) * feat(auth): build complete auth UI screens (Login, Register, OTP, Passwords) * chore(main): set LoginView as initial route * refactor(auth) : delete .gitkeep * chore: update dependencies and pubspec.lock * refactor(auth): optimize registration logic, timezone handling, and form validation * feat(auth): update UI for login, registration, and forgot password screens * feat(tasks): update task management UI and statistics screen * chore: update main entry point and fix widget tests * chore: ignore devtools_options.yaml * chore: ignore devtools_options.yaml * style(login) : rewrite title for login view * feat(auth): configure android deep link for supabase oauth * refactor(ui): add social login callbacks to auth layout template * feat(auth): update oauth methods with redirect url and signout * feat(auth): implement AuthGate using StreamBuilder for session tracking * feat(viewmodel): add oauth logic and improve provider lifecycle * refactor(ui): migrate LoginView to Provider pattern * chore(main): set AuthGate as initial route and setup provider * feat: implement full Focus feature set - Added Pomodoro timer with Start/Reset/Skip logic. - Integrated local Quick Notes with Pin/Delete functionality. - Supported image attachments in notes using image_picker. - Added Focus settings: time duration, vibration, and ringtones. * fix (auth) : dispose TextEditingControllers to prevent memory leaks * refactor (alarm ) : create off alarm button when time out * fix: apply CodeRabbit auto-fixes Fixed 3 file(s) based on 4 unresolved review comments. Co-authored-by: CodeRabbit <[email protected]> * fix(timer): prevent division by zero in progress calculation and sanitize negative settings input * fix(timer): prevent division by zero in progress calculation and sanitize negative settings input * fix(auth): unblock new-user login and add settings logout * refactor(LoginScreen) : compact all items to fit in screen to help users interface easily --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: CodeRabbit <[email protected]>
#36) Bumps [shared_preferences](https://github.com/flutter/packages/tree/main/packages/shared_preferences) from 2.5.4 to 2.5.5. - [Commits](https://github.com/flutter/packages/commits/shared_preferences-v2.5.5/packages/shared_preferences) --- updated-dependencies: - dependency-name: shared_preferences dependency-version: 2.5.5 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* feat(UserProfile): build screen UserProfile # Conflicts: # src/lib/features/main/view/screens/main_screen.dart * feat: switch dark/light theme * fix: black color theme * fix: black theme in statistics screen * feat: add dark theme to auth screen * feat: apply dark theme for bottom navigation bar * feat(RPC): update RPC to get data for heatmap * feat(RPC): update new RPC to get data for heatmap * feat: integrate chatbot assistant * feat(chatbot): integrate create task, answer question for chatbot * feat: remove mock data and get data tags and categories from supabase
📝 WalkthroughWalkthroughThis PR adds IDE configuration files (Visual Studio, VS Code), introduces a comprehensive task management feature set including task creation screens with state management via Changes
Sequence Diagram(s)sequenceDiagram
participant User as User
participant UI as CreateTaskScreen
participant Provider as CreateTaskProvider
participant Supabase as Supabase API
participant Feedback as SnackBar
User->>UI: Fill form (name, category, date, time, description)
User->>UI: Press "Confirm Task Creation"
UI->>Provider: onCreateTaskPressed(context)
Note over Provider: Validate task name (trimmed non-empty)
Provider->>Provider: Check Supabase user authenticated
Provider->>Provider: Set isLoading = true, notify listeners
Provider->>Provider: Map category label to category_id
Provider->>Provider: Combine date + start time
Provider->>Supabase: INSERT into task table
Supabase-->>Provider: Success/Error response
alt Success
Provider->>Feedback: Show "Task created successfully"
Provider->>UI: Navigator.pop(context)
else Failure
Provider->>Feedback: Show error message
end
Provider->>Provider: Set isLoading = false, notify listeners
sequenceDiagram
participant User as User
participant UI as TaskDetailScreen
participant ViewModel as TaskViewModel
participant Supabase as Supabase API
participant Dialog as AddNoteDialog
participant Feedback as SnackBar
User->>UI: View task details
UI->>ViewModel: Load categories, tags, notes (post-frame)
ViewModel->>Supabase: fetchTasks(), fetch categories/tags
Supabase-->>ViewModel: Task data, categories, tags
ViewModel-->>UI: Update state, notify listeners
User->>UI: Click add note icon
UI->>Dialog: Show _showAddNoteDialog
User->>Dialog: Enter note content
User->>Dialog: Confirm
Dialog->>ViewModel: createNote(taskId, content)
ViewModel->>Supabase: INSERT into note table
Supabase-->>ViewModel: Success/Error
ViewModel->>UI: Notify listeners
UI->>ViewModel: getNotesForTask(taskId) [FutureBuilder]
Supabase-->>ViewModel: Return notes list
ViewModel-->>UI: Render notes
User->>UI: Toggle tag chip
UI->>ViewModel: updateTaskTags(taskId, newTags)
ViewModel->>Supabase: UPDATE task table with new tags
Supabase-->>ViewModel: Success/Error
ViewModel-->>UI: Notify listeners
UI->>Feedback: Show success/error message
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes The changes introduce multiple new features across diverse files—two task creation flows with state management, note management with database operations, custom tag persistence, enhanced task detail views, and expanded Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
Note
Due to the large number of review comments, Critical severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
src/lib/features/auth/presentation/view/otp_verification_view.dart (1)
66-87:⚠️ Potential issue | 🔴 CriticalRender eight OTP boxes to match the 8-digit flow.
Line 86 still creates only 6 inputs while the text and focus logic expect 8 digits, so users cannot complete an 8-digit OTP in this screen.
Suggested fix
- Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: List.generate(6, (index) => _buildOtpBox(index, context)), - ), + Row( + children: List.generate( + 8, + (index) => Expanded( + child: Padding( + padding: EdgeInsets.only(right: index == 7 ? 0 : 6), + child: _buildOtpBox(index, context), + ), + ), + ), + ), @@ - width: 35, height: 48, // Thu nhỏ kích thước ô lại để nhét vừa 8 ô trên 1 dòng + width: double.infinity, + height: 48,Also applies to: 180-182, 198-200
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/features/auth/presentation/view/otp_verification_view.dart` around lines 66 - 87, The UI currently generates six OTP input boxes while the rest of the screen (text and focus/logic) expects an 8-digit code; update every List.generate(6, ...) that creates the OTP fields to List.generate(8, ...) so _buildOtpBox(index, context) is called for eight boxes (also update any hardcoded 6 occurrences around the _buildOtpBox usages at the other locations you noted). If there is an OTP length constant or variable used elsewhere, prefer using that (e.g., otpLength) instead of a magic number so all places stay consistent.src/lib/features/statistics/view/screens/statistics_screen.dart (1)
23-30:⚠️ Potential issue | 🟡 MinorGuard the deferred category load.
The post-frame callback can run after disposal, and
categories.isEmptystill allows duplicate fetches while another screen is already loading categories.Proposed fix
WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; final userId = Supabase.instance.client.auth.currentUser?.id; if (userId != null) { context.read<StatisticsViewmodel>().getStatisticsData(userId); final categoryViewModel = context.read<CategoryViewModel>(); - if (categoryViewModel.categories.isEmpty) { + if (categoryViewModel.categories.isEmpty && + !categoryViewModel.isLoading) { categoryViewModel.loadCategories(); } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/features/statistics/view/screens/statistics_screen.dart` around lines 23 - 30, Guard the post-frame callback against running after disposal and against duplicate category fetches: inside the callback (the WidgetsBinding.instance.addPostFrameCallback block) first check the widget is still mounted (return early if not), then get the CategoryViewModel and only call categoryViewModel.loadCategories() when categoryViewModel.categories.isEmpty AND a loading flag is false (e.g., categoryViewModel.isLoading or add one like categoryViewModel.isFetching) to prevent concurrent loads; keep the existing call to context.read<StatisticsViewmodel>().getStatisticsData(userId) but ensure it also runs only when mounted.src/lib/features/statistics/view/widgets/statistics_widgets.dart (1)
319-343:⚠️ Potential issue | 🟠 MajorAdd category_id to RecentTaskModel and use actual task category in CompletedTaskCard.
RecentTaskModelcurrently omits category data. Every completed task displayscategories.first, showing the wrong category for most tasks. Extend the RPC/statistics query to includecategory_id(orcategoryobject if available), then updateRecentTaskModelto accept and store it. Use this value when mapping the task, falling back to the first category only if missing.Fix TimeOfDay hour overflow when endTime is calculated.
Line 349 creates
TimeOfDay(hour: task.updatedAt.hour + 1, ...). When a task'supdatedAtis 23:xx, this produces an invalidTimeOfDay(hour: 24)which exceeds the valid range (0–23). Clamp or wrap the hour:hour: (task.updatedAt.hour + 1) % 24.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/features/statistics/view/widgets/statistics_widgets.dart` around lines 319 - 343, RecentTaskModel is missing category data so CompletedTaskCard always picks categoryViewModel.categories.first; extend the RPC/statistics query to return category_id (or category object), add a category/categoryId field to RecentTaskModel, populate it when deserializing, and update the mapping in CompletedTaskCard where you build TaskModel (the mappedTask creation) to use recentTask.category (or resolve its id to the correct CategoryModel) and only fall back to fallbackCategory when that field is absent. Also fix the TimeOfDay overflow where endTime is computed from task.updatedAt (currently TimeOfDay(hour: task.updatedAt.hour + 1, ...)) by wrapping/clamping the hour, e.g. use (task.updatedAt.hour + 1) % 24 (or min/max clamping) when constructing TimeOfDay to ensure hour stays in 0–23.
♻️ Duplicate comments (2)
.vs/TaskManagement.slnx/v18/DocumentLayout.json (1)
3-68:⚠️ Potential issue | 🟠 MajorLocal path and activity metadata leaked via tracked IDE layout file.
This file persists machine-specific paths and timestamps (for example, Line 3 and Lines 41/54/67). It should not be committed. Please remove tracked
.vslayout artifacts and keep.vs/ignored.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.vs/TaskManagement.slnx/v18/DocumentLayout.json around lines 3 - 68, The repo contains a tracked IDE layout JSON (DocumentLayout.json) that leaks machine-specific keys like "WorkspaceRootPath" and timestamp fields "WhenOpened"; remove that file from version control, add the IDE layout directory/pattern to .gitignore (so future files aren’t tracked), and commit the change; specifically run git rm --cached on the tracked DocumentLayout.json entry (or equivalent) and commit the removal alongside updating .gitignore to ignore the IDE layout files..vs/TaskManagement.slnx/v18/DocumentLayout.backup.json (1)
1-33:⚠️ Potential issue | 🟠 MajorSame issue: local IDE backup layout should not be tracked.
This backup file is user-specific workspace metadata and includes absolute local paths (for example, Line 3). Please remove it from git and rely on
.gitignorefor.vs/.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.vs/TaskManagement.slnx/v18/DocumentLayout.backup.json around lines 1 - 33, This file contains user-specific IDE workspace metadata (e.g., the "WorkspaceRootPath" entry) and should not be tracked; remove .vs/ layout backups from the repo by deleting this file from Git tracking (use git rm --cached on this DocumentLayout.backup.json or equivalent), add/ensure `.vs/` is listed in .gitignore, and commit the change so future workspace files (including entries like "WorkspaceRootPath") are not committed.
🟠 Major comments (24)
.vs/VSWorkspaceState.json-1-13 (1)
1-13:⚠️ Potential issue | 🟠 MajorDo not commit
.vsworkspace state files.This is local machine/editor state (for example, Line 11 selected node) and will create persistent merge noise across developers. Please remove this file from version control and ignore
.vs/in.gitignore.Suggested cleanup
# .gitignore +.vs/- .vs/VSWorkspaceState.json🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.vs/VSWorkspaceState.json around lines 1 - 13, The committed .vs workspace state (VSWorkspaceState.json containing keys like "ExpandedNodes" and "SelectedNode") is local editor state and must be removed from version control: delete the tracked .vs/VSWorkspaceState.json from the repo, add the .vs/ directory (or .vs/) to .gitignore, remove the file from the git index (so it stops being tracked) and commit the .gitignore and removal; ensure future commits do not re-add VSWorkspaceState.json by verifying .gitignore contains the .vs/ pattern.src/web/index.html-24-24 (1)
24-24:⚠️ Potential issue | 🟠 MajorUse
apple-mobile-web-app-capablefor iOS Safari/PWA support.Line 24 currently uses
mobile-web-app-capable, which is the Android/Chromium standard. The iOS-specific meta tag isapple-mobile-web-app-capable. Since this line is within the "iOS meta tags" section (alongside other apple-* prefixed tags), it should be corrected to ensure iOS home screen web apps function properly in standalone mode.Suggested fix
- <meta name="mobile-web-app-capable" content="yes"> + <meta name="apple-mobile-web-app-capable" content="yes">🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/web/index.html` at line 24, Replace the Android/Chromium meta tag name "mobile-web-app-capable" with the iOS-specific name "apple-mobile-web-app-capable" in the iOS meta tags section so the line that currently sets mobile-web-app-capable to "yes" uses the apple-prefixed meta name to enable proper iOS Safari/PWA standalone behavior.src/lib/features/chatbot/model/chatmessage_model.dart-14-23 (1)
14-23:⚠️ Potential issue | 🟠 MajorMake persisted chat history decoding tolerant of bad data.
jsonDecode(raw),Map<String, dynamic>.from(item), andjson['isUser'] as bool?can throw for corrupted or older persisted data, which can break chat startup instead of falling back to an empty/partial history.Suggested hardening
factory ChatMessageModel.fromJson(Map<String, dynamic> json) { final parsedTimestamp = DateTime.tryParse(json['timestamp']?.toString() ?? '') ?? DateTime.now(); + final rawIsUser = json['isUser']; return ChatMessageModel( text: json['text']?.toString() ?? '', - isUser: json['isUser'] as bool? ?? true, + isUser: rawIsUser is bool ? rawIsUser : true, timestamp: parsedTimestamp, ); } @@ static List<ChatMessageModel> decodeList(String raw) { - final decoded = jsonDecode(raw); - if (decoded is! List) return []; - - return decoded - .whereType<Map>() - .map((item) => ChatMessageModel.fromJson(Map<String, dynamic>.from(item))) - .toList(); + try { + final decoded = jsonDecode(raw); + if (decoded is! List) return []; + + final messages = <ChatMessageModel>[]; + for (final item in decoded.whereType<Map>()) { + try { + messages.add( + ChatMessageModel.fromJson(Map<String, dynamic>.from(item)), + ); + } catch (_) { + // Skip malformed entries instead of failing the whole history load. + } + } + return messages; + } catch (_) { + return []; + } }Also applies to: 37-45
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/features/chatbot/model/chatmessage_model.dart` around lines 14 - 23, The ChatMessageModel.fromJson should be made defensive so corrupted/old persisted data doesn't throw: wrap parsing in a try/catch and fall back to sane defaults, avoid using direct casts like json['isUser'] as bool? and Map<String, dynamic>.from; instead tolerate null/malformed json by checking types and coercing values (e.g., accept 'true'/'false' strings or 1/0 for isUser, parse timestamp with DateTime.tryParse and default to DateTime.now() on failure), and catch any exceptions to return a ChatMessageModel with safe defaults; apply the same hardening to the corresponding alternate constructor/decoder referenced around lines 37-45 (the other ChatMessageModel factory/decoder) so all persisted history decoding is tolerant of bad data.src/lib/features/category/view/widgets/category_choice_chips.dart-37-40 (1)
37-40:⚠️ Potential issue | 🟠 MajorChoose selected label color based on chip background contrast.
Line 40 hardcodes white text for every selected category. Light category colors can make the selected chip label unreadable.
Suggested contrast-aware fix
final category = categories[index]; final adaptiveColor = category.color.toAdaptiveColor(context); final isSelected = category.id == selectedCategoryId; + final selectedLabelColor = + ThemeData.estimateBrightnessForColor(adaptiveColor) == + Brightness.dark + ? Colors.white + : Colors.black; return Padding( @@ selectedColor: adaptiveColor, labelStyle: TextStyle( - color: isSelected ? Colors.white : adaptiveColor, + color: isSelected ? selectedLabelColor : adaptiveColor, fontSize: 14, ),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/features/category/view/widgets/category_choice_chips.dart` around lines 37 - 40, The selected label color is hardcoded to Colors.white which can be unreadable on light selected chip backgrounds; update the label color logic in the widget that builds the ChoiceChip (referenced by labelStyle, backgroundColor, selectedColor, isSelected and adaptiveColor) to pick a contrast-aware color when isSelected is true (e.g., use adaptiveColor.computeLuminance() to choose Colors.white for dark backgrounds and Colors.black for light backgrounds) while keeping the current adaptiveColor for the unselected state.src/lib/features/category/viewmodel/category_viewmodel.dart-22-38 (1)
22-38:⚠️ Potential issue | 🟠 MajorGuard against stale overlapping category loads.
loadCategories()can be triggered from multiple screens. If two calls overlap, the older request can finish last and overwrite newer data/error state, and_isLoadingcan flip tofalsewhile another request is still running.Suggested request-token guard
String? _error; String? get error => _error; + + int _loadRequestId = 0; Future<void> loadCategories() async { + final requestId = ++_loadRequestId; _isLoading = true; _error = null; notifyListeners(); try { final data = await _repository.fetchCategories(); + if (requestId != _loadRequestId) return; _categories ..clear() ..addAll(data); } catch (e) { + if (requestId != _loadRequestId) return; _error = e.toString(); _categories.clear(); } finally { + if (requestId != _loadRequestId) return; _isLoading = false; notifyListeners(); } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/features/category/viewmodel/category_viewmodel.dart` around lines 22 - 38, loadCategories can suffer from race conditions when multiple overlapping calls overwrite state; introduce a request token/id (e.g., _currentLoadId or _loadToken) that you increment before calling _repository.fetchCategories(), capture locally inside loadCategories(), and only apply results to _categories, _error and set _isLoading=false if the captured token equals the latest token; alternatively use an _activeLoadCount++/-- to keep _isLoading true while any load remains active. Update loadCategories to increment the token (or active count) before await, check the token after await (or decrement active count in finally) and only mutate state when the token matches (or active count hits zero), so older requests cannot overwrite newer results.src/lib/features/main/view/screens/create_task.dart-72-81 (1)
72-81:⚠️ Potential issue | 🟠 MajorDo not collect fields that are discarded on save.
The screen accepts
descriptionandendTime, but the insert only persists title/category/start timestamp. This silently loses user-entered task details; either persist these fields through the supported schema/RPC or remove the inputs until they are supported.Also applies to: 140-145, 279-316
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/features/main/view/screens/create_task.dart` around lines 72 - 81, The form collects description and endTime but the insert (via _supabase.from('task').insert) only saves title/category/create_at, causing silent data loss; either persist the extra fields by adding the correct column names to the insert payload (e.g., include 'description': _description.trim() and the proper timestamp column like 'end_at' or 'end_time': endTime.toIso8601String() matching your DB/RPC schema) and update any RPC calls used in _createTask/_saveTask methods, or remove/disable the description and endTime inputs from the CreateTask screen so the UI matches what is actually persisted (also apply the same fix to the other create/update blocks in this file that handle task inserts/updates).src/lib/features/main/view/screens/create_task.dart-54-61 (1)
54-61:⚠️ Potential issue | 🟠 MajorResolve categories from persisted data instead of hard-coded IDs.
Mapping display labels to fixed IDs can create tasks under the wrong category when database IDs differ between environments or users. Use the loaded category model/RPC result and submit the selected category’s actual ID.
Also applies to: 207-239
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/features/main/view/screens/create_task.dart` around lines 54 - 61, The code currently maps labels to hard-coded IDs using categoryMapping and assigns categoryId from _selectedCategory; instead, look up the persisted Category model returned by your RPC/fetch (e.g., the list variable that holds loaded categories such as fetchedCategories or categories) and find the matching category by its display label (or unique key) and use that Category.id when creating the task; replace the hard-coded categoryMapping and fallback logic so creation uses the real persisted ID and add a safe fallback (e.g., use the first category id or surface a validation error) if no match is found.src/pubspec.yaml-47-47 (1)
47-47:⚠️ Potential issue | 🟠 MajorMove the Gemini API key to a backend service instead of bundling it in the Flutter app.
The chatbot service loads
GEMINI_API_KEYfrom the bundled.envasset file. Since Flutter assets are extractable from the app package, this exposes the API key to any user who inspects the app. Use a Supabase Edge Function or other backend proxy to handle Gemini API calls instead.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pubspec.yaml` at line 47, Remove GEMINI_API_KEY from the bundled .env asset and stop initializing the chatbot service with a client-side Gemini key; instead implement a backend proxy (e.g., a Supabase Edge Function) that stores the Gemini API key securely and exposes an authenticated endpoint for chat requests, update the client-side ChatbotService (or wherever GEMINI_API_KEY is read) to call that backend endpoint (e.g., /edge-fn/gemini-chat) for all Gemini interactions, and ensure build/config no longer includes google_generative_ai credentials in assets or source so the key is never packaged with the Flutter app.src/lib/features/main/view/screens/main_screen.dart-38-42 (1)
38-42:⚠️ Potential issue | 🟠 Major
useMockData: trueis hard-coded in production initialization.
UserProfileViewModel(useMockData: true)is being wired into the live app shell at line 39. The constructor defaults totrue, but here it is explicitly set, ensuring the profile tab always loads mock data (via_buildMockUser()) instead of real user data fromUserService.fetchUserProfile(). This defeats the purpose of the UserService integration added in this PR.Before merging, gate this behind a build flag (
kDebugMode, environment variable, or--dart-define) so production builds use real data.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/features/main/view/screens/main_screen.dart` around lines 38 - 42, The UserProfileViewModel is being initialized with useMockData: true in the production app shell; change the initialization of UserProfileViewModel in the widget tree so useMockData is driven by a build-time/debug flag (e.g., use kDebugMode or a bool from environment via bool.fromEnvironment/--dart-define) instead of hard-coding true so that in production the constructor uses the real UserService.fetchUserProfile(); update the create call that constructs UserProfileViewModel (and its subsequent loadProfile()) to pass useMockData: <flag> (or omit if defaulting appropriately) and ensure UserProfileView remains untouched.src/lib/main.dart-36-49 (1)
36-49:⚠️ Potential issue | 🟠 MajorDon’t surface exception + stack trace in release builds.
ErrorWidget.builderis set unconditionally, so production users will see raw exception messages and stack traces on widget build failures. This is both a poor UX and a potential information-disclosure vector (internal paths, package names, SQL-looking messages from Supabase, etc.).Proposed fix — gate on `kDebugMode`
- ErrorWidget.builder = (FlutterErrorDetails details) { - return Material( - child: Container( - color: Colors.black87, - padding: const EdgeInsets.all(20), - child: SingleChildScrollView( - child: Text( - 'Lỗi!\n\n${details.exception}\n\nStack Trace:\n${details.stack}', - style: const TextStyle(color: Colors.greenAccent, fontSize: 14), - ), - ), - ), - ); - }; + if (kDebugMode) { + ErrorWidget.builder = (FlutterErrorDetails details) { + return Material( + child: Container( + color: Colors.black87, + padding: const EdgeInsets.all(20), + child: SingleChildScrollView( + child: Text( + 'Lỗi!\n\n${details.exception}\n\nStack Trace:\n${details.stack}', + style: const TextStyle(color: Colors.greenAccent, fontSize: 14), + ), + ), + ), + ); + }; + }Requires
import 'package:flutter/foundation.dart';.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/main.dart` around lines 36 - 49, ErrorWidget.builder is currently set unconditionally and exposes raw exceptions/stack traces from FlutterErrorDetails in production; wrap the custom ErrorWidget.builder assignment in a kDebugMode check (importing package:flutter/foundation.dart) so the detailed error UI is only used in debug, and provide a safe generic user-friendly fallback for release builds (e.g., a simple error message or empty Container) to avoid leaking internals.src/lib/features/user/service/user_service.dart-10-24 (1)
10-24:⚠️ Potential issue | 🟠 MajorDouble-wrapped exception handling obscures the original error in debug logs.
The
try/catchblock at lines 10–23 wraps every thrownException— including the twoExceptioninstances thrown inside thetryblock at lines 13 and 17 — into a genericException("Failed to fetch user profile: $e"). The resulting debug message becomes"Error loading profile: Exception: Failed to fetch user profile: Exception: Không tìm thấy...", burying the original exception type and losing the stack trace.Either drop the outer
try/catchentirely (let callers surface the original error) or userethrowto preserve the trace.Additionally, remove the stale comments on lines 7 and 9 (which claim to "simulate" and "mimic" a network delay, but the code performs real API calls without any artificial delay), and reuse the already-loaded
user.idinstead of redundantly callingcurrentUser!.idagain on line 19.Proposed fix
- Future<UserProfileModel> fetchUserProfile() async { - // Mimic API call delay for smooth state switching - try{ - final user = _supabase.auth.currentUser; - if (user == null) { - throw Exception("Không tìm thấy phiên đăng nhập. Hãy đăng nhập lại"); - } - final response = await _supabase.rpc('get_user_profile_stats'); - if(response == null){ - throw Exception("Không thể lấy thông tin người dùng. Hãy thử lại sau"); - } - response['id'] = _supabase.auth.currentUser!.id; - return UserProfileModel.fromJson(response); - } - catch(e){ - throw Exception("Failed to fetch user profile: $e"); - } - } + Future<UserProfileModel> fetchUserProfile() async { + final user = _supabase.auth.currentUser; + if (user == null) { + throw Exception('Không tìm thấy phiên đăng nhập. Hãy đăng nhập lại'); + } + final response = await _supabase.rpc('get_user_profile_stats'); + if (response == null) { + throw Exception('Không thể lấy thông tin người dùng. Hãy thử lại sau'); + } + final map = Map<String, dynamic>.from(response as Map); + map['id'] = user.id; + return UserProfileModel.fromJson(map); + }This also defensively copies the RPC response into a new
Map<String, dynamic>before mutating, in case the returned map is immutable.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/features/user/service/user_service.dart` around lines 10 - 24, The catch block in the user profile fetch wraps and masks original exceptions; remove the outer try/catch or replace the catch with a rethrow to preserve stack traces, reuse the already-loaded user variable for the id (use user.id instead of calling _supabase.auth.currentUser!.id), defensively copy the RPC result into a new Map<String, dynamic> before mutating (so you don’t modify an immutable response) and remove the stale "simulate"/"mimic" network delay comments; locate changes around the _supabase usage and UserProfileModel.fromJson call to update error handling, response copying, and id assignment.supabase/migrations/20260417060333_chatbot_add_task_rpc.sql-46-50 (1)
46-50:⚠️ Potential issue | 🟠 MajorAvoid returning raw database errors to clients.
SQLERRMcan expose table names, constraint names, and policy details. Return a stable error code/message and log the internal error server-side.Proposed fix
EXCEPTION WHEN OTHERS THEN + RAISE LOG 'create_task_full failed for user %: %', auth.uid(), SQLERRM; RETURN json_build_object( 'success', false, - 'error', SQLERRM + 'error', 'CREATE_TASK_FAILED' );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@supabase/migrations/20260417060333_chatbot_add_task_rpc.sql` around lines 46 - 50, The EXCEPTION WHEN OTHERS THEN block currently returns raw SQLERRM via json_build_object which can leak internal schema details; change this to log the full SQLERRM/server error internally (e.g., RAISE NOTICE/EXCEPTION to a server log, INSERT into an audit/errors table, or call a logging function) and return a stable, non-sensitive payload instead (e.g., json_build_object('success', false, 'code', 'INTERNAL_ERROR', 'message', 'An unexpected error occurred')). Update the EXCEPTION WHEN OTHERS THEN handling that references SQLERRM and json_build_object to perform internal logging and return the generic error object.src/lib/features/tag/view/widgets/tag_selector.dart-24-59 (1)
24-59:⚠️ Potential issue | 🟠 MajorPop the dialog context, not the parent context, after async work.
If the user dismisses the dialog while
addCustomTagis pending,Navigator.pop(context)can pop the underlying screen. Capture the dialog context and check that it is still mounted.Proposed fix
showDialog( context: context, - builder: (_) => AlertDialog( + builder: (dialogContext) => AlertDialog( @@ TextButton( - onPressed: () => Navigator.pop(context), + onPressed: () => Navigator.pop(dialogContext), child: const Text('Huỷ'), ), @@ if (error != null) { ScaffoldMessenger.of(context).showSnackBar( @@ ); } else { - Navigator.pop(context); + if (dialogContext.mounted) { + Navigator.pop(dialogContext); + } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/features/tag/view/widgets/tag_selector.dart` around lines 24 - 59, The dialog is popping the parent context after awaiting viewModel.addCustomTag; update _showAddCustomDialog to capture the dialog-specific BuildContext by changing the showDialog builder signature to (dialogContext) => AlertDialog(...) and replace Navigator.pop(context) inside the async handler with Navigator.of(dialogContext).pop(), but only after verifying the dialog navigator is still mounted (e.g., if (Navigator.of(dialogContext).mounted) Navigator.of(dialogContext).pop()); keep references to _showAddCustomDialog and viewModel.addCustomTag to locate the changes.supabase/migrations/20260413084521_update_user_profile_with_heatmap_rpc.sql-6-35 (1)
6-35:⚠️ Potential issue | 🟠 MajorClamp
p_daysand compare local dates consistently.A caller can pass a huge
p_daysvalue and force a large scan. Also, the currentupdated_at >= (...)::DATEcomparison converts a Vietnam-local date back through the DB timezone, which can drop boundary-day completions.Proposed fix
DECLARE v_user_id UUID; v_username TEXT; v_avatar TEXT; v_tasks_done INT; v_current_streak INT; v_heatmap_data JSON; + v_days INT; BEGIN v_user_id := auth.uid(); + v_days := LEAST(GREATEST(COALESCE(p_days, 90), 1), 365); IF v_user_id IS NULL THEN RAISE EXCEPTION 'User not authenticated'; @@ SELECT DATE(updated_at AT TIME ZONE 'Asia/Ho_Chi_Minh') AS task_date, COUNT(*) AS task_count FROM public.task WHERE profile_id = v_user_id AND status = 1 - AND updated_at >= ((CURRENT_TIMESTAMP AT TIME ZONE 'Asia/Ho_Chi_Minh')::DATE - (p_days || ' days')::INTERVAL) + AND DATE(updated_at AT TIME ZONE 'Asia/Ho_Chi_Minh') + >= (CURRENT_TIMESTAMP AT TIME ZONE 'Asia/Ho_Chi_Minh')::DATE - v_days GROUP BY DATE(updated_at AT TIME ZONE 'Asia/Ho_Chi_Minh')🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@supabase/migrations/20260413084521_update_user_profile_with_heatmap_rpc.sql` around lines 6 - 35, Clamp the incoming p_days to a safe range (e.g., min 1, max 365) and use that clamped value when building the interval; replace direct (p_days || ' days')::INTERVAL with a sanitized integer (e.g., v_days) and use INTERVAL format based on that value. Also compare dates in the same local timezone by converting updated_at to 'Asia/Ho_Chi_Minh' and comparing its DATE to (CURRENT_TIMESTAMP AT TIME ZONE 'Asia/Ho_Chi_Minh')::DATE - v_days (or equivalent interval), so the WHERE clause that computes task_date (DATE(updated_at AT TIME ZONE 'Asia/Ho_Chi_Minh')) is compared against a local DATE boundary computed the same way to avoid timezone boundary drops; update references in the heatmap query that produce v_heatmap_data and any earlier uses of p_days accordingly.src/lib/features/statistics/view/widgets/statistics_widgets.dart-344-351 (1)
344-351:⚠️ Potential issue | 🟠 MajorHandle
TimeOfDayrollover at 23:xx.
task.updatedAt.hour + 1becomes24for tasks completed at 23:xx, which is outside Flutter's validTimeOfDayhour range (0–23). The code at line 349 will throw a runtime error when a task is updated at this time.Proposed fix
onTap: () { + final endDateTime = task.updatedAt.add(const Duration(hours: 1)); final mappedTask = TaskModel( @@ endTime: TimeOfDay( - hour: task.updatedAt.hour + 1, - minute: task.updatedAt.minute, + hour: endDateTime.hour, + minute: endDateTime.minute, ),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/features/statistics/view/widgets/statistics_widgets.dart` around lines 344 - 351, The endTime construction can produce an invalid hour (24) when task.updatedAt.hour is 23; update the TimeOfDay creation to wrap hours into the valid 0–23 range (e.g., compute endHour = (task.updatedAt.hour + 1) % 24 or conditional if task.updatedAt.hour == 23 then 0) and use that endHour with task.updatedAt.minute when building the endTime TimeOfDay to avoid runtime errors in TimeOfDay(hour: ..., minute: ...).src/lib/features/tasks/view/screens/home_screen.dart-105-109 (1)
105-109:⚠️ Potential issue | 🟠 MajorTheming regression: hardcoded
AppColors/Colors.black/Colors.whiteacross the screen.The stated intent of this PR is a theme refactor, but
HomeScreenstill hardcodes brand/system colors in several spots, so the screen renders poorly in dark mode and bypasses the newColorScheme:
- Line 109:
Container(color: AppColors.primaryBlue)for the top wave — should useTheme.of(context).colorScheme.primary.- Lines 170, 173, 181:
Icon(..., color: Colors.black)for menu/notifications/add — invisible on dark backgrounds; usecolorScheme.onSurface(oronPrimaryif the wave sits behind them).- Line 224:
AppColors.grayText→colorScheme.onSurfaceVariant.- Line 268:
_FilterChip(color: AppColors.primaryBlue, ...)— should usecolorScheme.primary.- Lines 389, 398:
_FilterChipusesColors.whitefor unselected bg / selected text — breaks in dark mode; prefercolorScheme.surface/colorScheme.onPrimary.Once these are removed, the
app_colors.dartimport on Line 5 should go too.Also applies to: 167-199, 223-224, 265-270, 385-400
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/features/tasks/view/screens/home_screen.dart` around lines 105 - 109, HomeScreen still uses hardcoded AppColors/Colors (e.g., Container(color: AppColors.primaryBlue) in the TopWaveClipper area, Icon(..., color: Colors.black), AppColors.grayText, and _FilterChip(color: AppColors.primaryBlue/Colors.white)), which breaks dark mode; update these usages to read from Theme.of(context).colorScheme (use colorScheme.primary for the wave and _FilterChip selected color, colorScheme.onPrimary for icons on the wave, colorScheme.onSurface/onSurfaceVariant for surface text/icons, and colorScheme.surface for chip backgrounds) and adjust _FilterChip props accordingly so selected/unselected states use colorScheme.surface and colorScheme.onPrimary/onSurface as appropriate; after replacing all occurrences in HomeScreen and _FilterChip, remove the unused app_colors.dart import.src/lib/features/tasks/view/screens/task_detail_screen.dart-220-250 (1)
220-250:⚠️ Potential issue | 🟠 MajorDelete flow uses a popped context and ignores async failures.
Three issues on the delete path:
context.read<TaskViewModel>().deleteTask(widget.task.id)is not awaited, soNavigator.pop(context)runs before the delete completes; if the RPC fails, the task is already removed from the UI stack and the error is swallowed.ScaffoldMessenger.of(context).showSnackBar(...)at Line 240 is called AFTERNavigator.pop(context)at Line 239 — the detail screen is being torn down, so the snackbar frequently shows on the wrong scaffold or not at all.- No
mountedcheck after the async work.Await the delete, capture the messenger before popping, and show the snackbar via the parent scaffold.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/features/tasks/view/screens/task_detail_screen.dart` around lines 220 - 250, The delete path should await the async delete, capture the ScaffoldMessenger before popping, and check mounted before navigating: change the onPressed handler inside the AlertDialog's "Xóa" button to await context.read<TaskViewModel>().deleteTask(widget.task.id) (referencing TaskViewModel.deleteTask and widget.task.id), call final messenger = ScaffoldMessenger.of(context) before any Navigator.pop calls, then after the await verify if (!mounted) return; before calling Navigator.pop(context) and Navigator.pop(ctx); use messenger.showSnackBar(...) to show success or show an error SnackBar if the awaited delete throws (catch the exception and show a failure message) so the UI isn't popped prematurely and snackbar targets the correct scaffold.src/lib/features/chatbot/services/chatbot_services.dart-72-104 (1)
72-104:⚠️ Potential issue | 🟠 MajorUnsafe casts and unvalidated RPC inputs; catch-all hides real failures as "AI connection error".
A few concerns in the function-call branch:
- Line 76:
args['title'] as Stringwill throw if Gemini returnsnullor a non-string fortitle. Any such failure propagates to the genericcatch (e)at Line 106 and is reported to the user asLỗi kết nối AI: …, which is misleading (it's a schema violation, not a network error).- Line 77:
priorityis clamped via?? 1but not range-checked. The RPCcreate_task_fullaccepts anyINT4and stores it as-is, so a model hallucination likepriority: 99silently corrupts task data. Clamp to[1, 3]before calling the RPC.- Line 96:
dbResponse['success']assumes the RPC response is aMap. Supabaserpc(...)returnsdynamic; if the function errors out or returnsnull, this throws and again surfaces as a connection error.- Line 98: Sending a
functionResponsewith only{'status': 'Thành công' | 'Thất bại'}drops the RPC'serror/task_id/messagefields, so the model cannot tailor its reply (or apologize with a reason on failure). Forward the relevant payload.🛡️ Proposed hardening
- final title = args['title'] as String; - final priority = (args['priority'] as num?)?.toInt() ?? 1; - final rawTags = args['tags'] as List<dynamic>? ?? []; - final tags = rawTags.map((e) => e.toString()).toList(); + final title = (args['title'] as String?)?.trim() ?? ''; + if (title.isEmpty) { + return 'Mình chưa rõ tên công việc, bạn nói lại giúp nhé!'; + } + final rawPriority = (args['priority'] as num?)?.toInt() ?? 1; + final priority = rawPriority.clamp(1, 3); + final rawTags = args['tags'] as List<dynamic>? ?? const []; + final tags = rawTags.map((e) => e.toString()).toList(); @@ - final isSuccess = dbResponse['success'] == true; + final responseMap = (dbResponse is Map) ? dbResponse : const {}; + final isSuccess = responseMap['success'] == true; final functionResponse = await _chatSession!.sendMessage( Content.functionResponse('create_task_full', { - 'status': isSuccess ? 'Thành công' : 'Thất bại', + 'status': isSuccess ? 'Thành công' : 'Thất bại', + if (!isSuccess && responseMap['error'] != null) + 'error': responseMap['error'], }), );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/features/chatbot/services/chatbot_services.dart` around lines 72 - 104, In the function-call branch handling response.functionCalls (inside the block that checks functionCall.name == 'create_task_full'), validate and safely parse args: ensure title is a non-empty String (return a clear user-facing validation message if missing), parse priority as int then clamp it to the allowed range 1..3, and coerce tags to List<String> safely; then call Supabase.instance.client.rpc with these sanitized inputs. After RPC, defensively handle dbResponse being null or not a Map (check type and presence of keys), extract and forward relevant RPC fields (e.g., 'success', 'task_id', 'message', 'error') into the Content.functionResponse payload instead of only {'status': ...} so the model gets full context, and ensure any thrown parsing/validation errors are reported with an appropriate message rather than bubbled to the generic AI-connection catch-all; update the use of _chatSession!.sendMessage(Content.functionResponse('create_task_full', ...)) accordingly.src/lib/features/tasks/view/screens/task_detail_screen.dart-130-157 (1)
130-157:⚠️ Potential issue | 🟠 Major
FutureBuilder.futurerecreated on every rebuild.
future: context.read<TaskViewModel>().getNotesForTask(taskId)is evaluated inline, so every rebuild of the detail screen (tag toggle, category change, time picker, keyboard open/close, etc.) kicks off a new Supabase query and briefly showsCircularProgressIndicator, causing flicker and unnecessary network traffic. Store theFuturein state and only refresh it in response to explicit events (e.g., aftercreateNotesucceeds).♻️ Sketch
-class _TaskDetailScreenState extends State<TaskDetailScreen> { +class _TaskDetailScreenState extends State<TaskDetailScreen> { + Future<List<NoteModel>>? _notesFuture; @@ - _currentTags = List.from(widget.task.tags); + _currentTags = List.from(widget.task.tags); + _notesFuture = context.read<TaskViewModel>().getNotesForTask(widget.task.id.toString()); @@ - FutureBuilder<List<NoteModel>>( - future: context.read<TaskViewModel>().getNotesForTask(taskId), + FutureBuilder<List<NoteModel>>( + future: _notesFuture,Then refresh
_notesFutureafter a successfulcreateNoteinstead of calling baresetState(() {}).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/features/tasks/view/screens/task_detail_screen.dart` around lines 130 - 157, The FutureBuilder is recreating the future each rebuild because you call context.read<TaskViewModel>().getNotesForTask(taskId) inline; introduce a State field (e.g., Future<List<NoteModel>>? _notesFuture) in the TaskDetailScreen State, initialize it once in initState with _notesFuture = context.read<TaskViewModel>().getNotesForTask(taskId), and change the FutureBuilder to use future: _notesFuture; after a successful createNote (or other explicit refresh events) reassign _notesFuture = context.read<TaskViewModel>().getNotesForTask(taskId) and call setState to update—this prevents automatic refetch/flicker on unrelated rebuilds.src/lib/features/tasks/viewmodel/task_viewmodel.dart-162-167 (1)
162-167:⚠️ Potential issue | 🟠 MajorClear stale tasks when there is no authenticated user.
If the session is missing,
fetchTasks()returns without clearing_tasks, so a logged-out or switched user can still see the previous user’s in-memory task list.Proposed fix
final user = supabase.auth.currentUser; - if (user == null) return; + if (user == null) { + _tasks.clear(); + notifyListeners(); + return; + }Also applies to: 211-213
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/features/tasks/viewmodel/task_viewmodel.dart` around lines 162 - 167, When fetchTasks() finds no authenticated user (Supabase.instance.client.auth.currentUser == null), it should clear the in-memory task list and update observers instead of returning early; update the fetchTasks() method to set _tasks = [] (or clear the collection) and call notifyListeners() before returning. Apply the same change to the other fetch path referenced in the file (the second early-return block around the later fetchTasks logic) so that both locations clear _tasks and notifyListeners when user == null.src/lib/features/tasks/viewmodel/task_viewmodel.dart-169-206 (1)
169-206:⚠️ Potential issue | 🟠 MajorHydrate category and tags from the persisted schema.
fetchTasks()selects*and readsitem['category'], but task creation writescategory_id; fetched tasks will fall back toGeneral. Tags are never populated either, so tag filtering overt.tagscannot work for persisted tasks.Proposed direction
final data = await supabase .from('task') - .select('*') + .select(''' + *, + category:category_id(id, name, color_code, profile_id), + tags:task_tag(tag(id, name, color_code, profile_id)) + ''') .eq('profile_id', user.id) .order('create_at', ascending: true);Then build
CategoryModelfrom the joinedcategoryobject andList<TagModel>from the joined tag rows instead of using placeholder category data and empty tags.Also applies to: 259-264
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/features/tasks/viewmodel/task_viewmodel.dart` around lines 169 - 206, fetchTasks() currently reads item['category'] incorrectly (only category_id was persisted) and never populates TaskModel.tags, so tasks fall back to a placeholder CategoryModel and empty tags. Fix by updating the Supabase query in fetchTasks() to include the joined category and tags (e.g. select with category(*) and tags(*) or the intermediate task_tag join), then when iterating items construct CategoryModel from the returned category object fields (id, name, color_code, profile_id) instead of the hardcoded values and build a List<TagModel> from the returned tag rows (mapping each tag row to TagModel) and assign it to TaskModel.tags; keep using existing TaskModel, CategoryModel and TagModel constructors to map the fields and preserve date/priority parsing as before.src/lib/features/user/viewmodel/user_profile_viewmodel.dart-16-16 (1)
16-16:⚠️ Potential issue | 🟠 MajorDefault profile loading should use real data.
useMockDatadefaults totrue, so any production call site that omits the argument will show the hardcodedAlex Thompsonprofile instead of the signed-in user. Make mock mode opt-in and ensure the production provider wiring does not passtrue.Proposed fix
- UserProfileViewModel({this.useMockData = true}); + UserProfileViewModel({this.useMockData = false});Also applies to: 30-34
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/features/user/viewmodel/user_profile_viewmodel.dart` at line 16, The constructor default for UserProfileViewModel currently sets useMockData = true, causing real callers to load mock profiles; change the default to false so mock mode is opt-in (i.e., UserProfileViewModel({this.useMockData = false})), update any related factory/constructors or providers that currently pass true to instead opt-in explicitly, and scan the other constructor/initialization usages referenced around the UserProfileViewModel class (the similar block at lines ~30-34) to ensure they do not default to or pass true inadvertently.src/lib/features/tasks/view/screens/create_task_screen.dart-167-172 (1)
167-172:⚠️ Potential issue | 🟠 MajorRemove demo defaults and dispose the controllers.
The create screen currently opens with
"Team Meeting"and a sample description, which can create accidental test data. Since these controllers are owned by the state object, they should also be disposed.Proposed fix
- final TextEditingController _nameController = TextEditingController( - text: 'Team Meeting', - ); - final TextEditingController _descController = TextEditingController( - text: 'Discuss all questions about new projects', - ); + final TextEditingController _nameController = TextEditingController(); + final TextEditingController _descController = TextEditingController(); + + `@override` + void dispose() { + _nameController.dispose(); + _descController.dispose(); + super.dispose(); + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/features/tasks/view/screens/create_task_screen.dart` around lines 167 - 172, Remove the hardcoded demo values from the TextEditingController instances by initializing _nameController and _descController without the text: parameter so they start empty, and add a dispose override in the State class to call _nameController.dispose() and _descController.dispose(); ensure the dispose method also calls super.dispose(). These symbols to change are the TextEditingController fields named _nameController and _descController and the State's dispose() method for the CreateTaskScreen widget.src/lib/features/tasks/view/screens/create_task_screen.dart-66-127 (1)
66-127:⚠️ Potential issue | 🟠 MajorPersist all submitted task fields.
descriptionandtagsare accepted bysubmitTask()and passed from the UI, but only title/status/priority/profile/category/date are inserted. This drops the user’s description and selected tags on every created task.Proposed direction
- await _supabase.from('task').insert({ + final insertedTask = await _supabase.from('task').insert({ 'title': taskName.trim(), - //'description': description.trim(), // Tui mở comment cái này ra cho ông luôn + 'description': description.trim(), 'status': 0, 'priority': priorityId, 'profile_id': user.id, 'category_id': categoryId, // Dùng ID thực tế từ UI 'create_at': scheduledDateTime.toIso8601String(), - }); + }).select('id').single(); + + // Also persist `tags` into the task/tag join table used by the schema. + // Example: + // await _supabase.from('task_tag').insert( + // tags.map((tag) => {'task_id': insertedTask['id'], 'tag_id': tag.id}).toList(), + // );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/features/tasks/view/screens/create_task_screen.dart` around lines 66 - 127, The insert currently omits the passed description and tags; update the insert in submitTask(...) (the _supabase.from('task').insert call) to include 'description' (use description.trim() or null-guard if empty) and persist 'tags' (serialize to the DB format your table expects, e.g., a JSON array or text array—convert List<dynamic> tags to List<String> or jsonEncode(tags) as needed) alongside the existing fields so all submitted task fields are saved.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 72b404c1-009d-4cd9-8339-2fbc84279dbf
⛔ Files ignored due to path filters (6)
src/pubspec.lockis excluded by!**/*.locksrc/web/favicon.pngis excluded by!**/*.pngsrc/web/icons/Icon-192.pngis excluded by!**/*.pngsrc/web/icons/Icon-512.pngis excluded by!**/*.pngsrc/web/icons/Icon-maskable-192.pngis excluded by!**/*.pngsrc/web/icons/Icon-maskable-512.pngis excluded by!**/*.png
📒 Files selected for processing (72)
.vs/TaskManagement.slnx/v18/.wsuo.vs/TaskManagement.slnx/v18/DocumentLayout.backup.json.vs/TaskManagement.slnx/v18/DocumentLayout.json.vs/VSWorkspaceState.json.vs/slnx.sqlite.vscode/launch.jsonREADME.mdsrc/.gitignoresrc/lib/core/theme/app_theme.dartsrc/lib/core/theme/auth_layout_template.dartsrc/lib/core/theme/custom_text_field.dartsrc/lib/core/theme/theme_provider.dartsrc/lib/core/utils/adaptive_color_extension.dartsrc/lib/core/widgets/custom_input_field.dartsrc/lib/features/auth/otp_verification_view.dartsrc/lib/features/auth/presentation/view/forgot_password_view.dartsrc/lib/features/auth/presentation/view/login_view.dartsrc/lib/features/auth/presentation/view/new_password_view.dartsrc/lib/features/auth/presentation/view/otp_verification_view.dartsrc/lib/features/auth/presentation/view/register_view.dartsrc/lib/features/auth/viewmodels/task_provider.dartsrc/lib/features/category/model/category_model.dartsrc/lib/features/category/repository/category_repository.dartsrc/lib/features/category/view/widgets/category_choice_chips.dartsrc/lib/features/category/viewmodel/category_viewmodel.dartsrc/lib/features/chatbot/model/chatmessage_model.dartsrc/lib/features/chatbot/services/chatbot_services.dartsrc/lib/features/chatbot/view/chatbot_view.dartsrc/lib/features/chatbot/view/widgets/bot_avatar.dartsrc/lib/features/chatbot/view/widgets/chat_header.dartsrc/lib/features/chatbot/view/widgets/day_separator.dartsrc/lib/features/chatbot/view/widgets/message_composer.dartsrc/lib/features/chatbot/view/widgets/message_tile.dartsrc/lib/features/chatbot/view/widgets/typing_indicator.dartsrc/lib/features/chatbot/view/widgets/user_avatar.dartsrc/lib/features/chatbot/viewmodel/chatbot_viewmodel.dartsrc/lib/features/main/view/screens/create_task.dartsrc/lib/features/main/view/screens/main_screen.dartsrc/lib/features/note/view/focus_screen.dartsrc/lib/features/note/view/focus_widget.dartsrc/lib/features/statistics/model/StatisticsModel.dartsrc/lib/features/statistics/view/screens/statistics_screen.dartsrc/lib/features/statistics/view/widgets/statistics_widgets.dartsrc/lib/features/tag/model/tag_model.dartsrc/lib/features/tag/repository/tag_repository.dartsrc/lib/features/tag/view/widgets/tag_selector.dartsrc/lib/features/tag/viewmodel/tag_viewmodel.dartsrc/lib/features/tasks/model/task_model.dartsrc/lib/features/tasks/view/screens/create_task_screen.dartsrc/lib/features/tasks/view/screens/home_screen.dartsrc/lib/features/tasks/view/screens/task_detail_screen.dartsrc/lib/features/tasks/view/widgets/priority_selector.dartsrc/lib/features/tasks/view/widgets/tag_selector.dartsrc/lib/features/tasks/view/widgets/task_widgets.dartsrc/lib/features/tasks/viewmodel/task_viewmodel.dartsrc/lib/features/user/model/user_profile_model.dartsrc/lib/features/user/service/user_service.dartsrc/lib/features/user/view/user_profile_view.dartsrc/lib/features/user/view/widgets/logout_button.dartsrc/lib/features/user/view/widgets/profile_header.dartsrc/lib/features/user/view/widgets/settings_list_tile.dartsrc/lib/features/user/view/widgets/settings_section.dartsrc/lib/features/user/view/widgets/stat_card.dartsrc/lib/features/user/viewmodel/user_profile_viewmodel.dartsrc/lib/main.dartsrc/pubspec.yamlsrc/web/index.htmlsrc/web/manifest.jsonsrc/web_entrypoint.dartsupabase/migrations/20260409084009_create_user_profile_rpc.sqlsupabase/migrations/20260413084521_update_user_profile_with_heatmap_rpc.sqlsupabase/migrations/20260417060333_chatbot_add_task_rpc.sql
| void _toggleTag(TagModel tag) { | ||
| setState(() { | ||
| if (_currentTags.any((t) => t.id == tag.id)) { | ||
| _currentTags.removeWhere((t) => t.id == tag.id); | ||
| } else { | ||
| _currentTags.add(tag); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| bool _isTagSelected(TagModel tag) => _currentTags.any((t) => t.id == tag.id); |
There was a problem hiding this comment.
Tag/time/description edits are silently dropped on Save.
State is tracked for tags (_currentTags), times (_startTime/_endTime) and description (_descController), and the UI lets users edit all of them — but _saveChanges only sends title and category_id:
final Map<String, dynamic> updates = {
'title': _titleController.text.trim(),
'category_id': _currentCategory.id,
};So toggling a tag chip, picking a new start/end time, or editing the description appears to save (snackbar "Cập nhật thành công!") but the mutations never reach Supabase. This is data-loss from the user's perspective. Either wire the remaining fields into updates (and the task_tags join table for tag add/remove) or disable the inputs.
Also applies to: 161-183
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/lib/features/tasks/view/screens/task_detail_screen.dart` around lines 56
- 66, The save routine currently only sends title and category_id; update
_saveChanges to include description (_descController.text.trim()), start/end
times (_startTime/_endTime) converted to the expected payload format, and the
tag changes derived from _currentTags; additionally, implement syncing for the
task_tags join table by computing tags to add/remove (compare original tags vs
_currentTags) and call the appropriate Supabase mutations or API methods to
insert/delete join rows (or include tag IDs in the task update if your backend
supports it). Ensure you reference the existing _currentTags, _startTime,
_endTime, _descController, and _saveChanges symbols when adding the payload and
tag-sync logic so edited tags, times, and description are actually persisted
rather than silently dropped.
| CREATE OR REPLACE FUNCTION create_task_full( | ||
| p_title TEXT, | ||
| p_priority INT4, | ||
| p_profile_id UUID, | ||
| p_tag_names TEXT[] | ||
| ) | ||
| RETURNS JSON | ||
| LANGUAGE plpgsql | ||
| AS $$ | ||
| DECLARE | ||
| v_task_id INT8; | ||
| v_tag_name TEXT; | ||
| v_tag_id INT8; | ||
| BEGIN | ||
|
|
||
| INSERT INTO task (title, priority, profile_id, status) | ||
| VALUES (p_title, p_priority, p_profile_id, 0) | ||
| RETURNING id INTO v_task_id; |
There was a problem hiding this comment.
Don’t trust p_profile_id from the client.
Any authenticated caller can invoke this RPC with another profile ID unless every RLS policy blocks it. Derive or verify the profile ID from auth.uid() before inserting.
Proposed fix
DECLARE
v_task_id INT8;
v_tag_name TEXT;
v_tag_id INT8;
+ v_profile_id UUID;
BEGIN
+ v_profile_id := auth.uid();
+
+ IF v_profile_id IS NULL OR p_profile_id IS DISTINCT FROM v_profile_id THEN
+ RAISE EXCEPTION 'Not authorized to create task for this profile';
+ END IF;
INSERT INTO task (title, priority, profile_id, status)
- VALUES (p_title, p_priority, p_profile_id, 0)
+ VALUES (p_title, p_priority, v_profile_id, 0)
RETURNING id INTO v_task_id;
@@
INSERT INTO tag (name, profile_id, color_code)
- VALUES (v_tag_name, p_profile_id, '#6200EE')
+ VALUES (v_tag_name, v_profile_id, '#6200EE')Also applies to: 26-34
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@supabase/migrations/20260417060333_chatbot_add_task_rpc.sql` around lines 1 -
18, The RPC create_task_full must not trust the client-supplied p_profile_id;
instead derive or verify the profile ID using auth.uid() before inserting.
Inside create_task_full, query the profile table (e.g., SELECT id INTO
v_profile_id FROM profile WHERE auth_uid = auth.uid()) or validate that
p_profile_id matches the profile id for auth.uid(), then use that
server-verified v_profile_id in the INSERT (replace uses of p_profile_id).
Ensure any error path returns or raises when no profile is found or the
verification fails so unprivileged callers cannot insert for another profile.
There was a problem hiding this comment.
Actionable comments posted: 7
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/lib/features/tasks/view/screens/create_task_screen.dart (1)
485-500:⚠️ Potential issue | 🟡 MinorRemove the duplicate Description field.
The same
_descControlleris rendered twice, which makes the form look broken and can confuse users.Proposed fix
- // --- Input Desc --- - CustomInputField( - label: 'Description', - hint: 'Enter task description', - controller: _descController, - maxLines: 2, - ), - const SizedBox(height: 25), - // Description CustomInputField( label: 'Description',🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/features/tasks/view/screens/create_task_screen.dart` around lines 485 - 500, The UI renders the same Description input twice using CustomInputField with the same _descController; remove the duplicate widget instance so only one Description field remains (keep a single CustomInputField that uses _descController and its surrounding spacing like the SizedBox), and verify CreateTaskScreen (or the widget tree containing _descController) no longer contains the duplicated block to avoid controller reuse and visual duplication.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/lib/features/tasks/view/screens/create_task_screen.dart`:
- Around line 66-127: The submitTask function drops the description and tags
parameters when inserting into Supabase, so include description in the task
insert and persist tags after the task is created: add 'description':
description.trim() to the payload passed to _supabase.from('task').insert,
capture the inserted task's id from the insert response, then persist tags by
inserting into your join/association (e.g., _supabase.from('task_tag').insert)
using that task id and each tag id or name; if you intend to store tags as a
JSON/array column on the task row instead, write the tags list (e.g.,
tags.map(...).toList()) into that column in the same insert. Ensure you handle
failures and keep _isLoading/notifyListeners consistent.
- Line 193: CreateTaskScreen is calling context.read<CreateTaskProvider>()
(e.g., in CreateTaskScreen where formattedDate is computed and at other
occurrences) but CreateTaskProvider is not registered, causing
ProviderNotFoundException; fix by registering the provider - either add
ChangeNotifierProvider<CreateTaskProvider>(create: (_) => CreateTaskProvider())
to your app's root MultiProvider so the provider is available app-wide, or wrap
the screen when navigating (replace MaterialPageRoute(builder: (_) =>
CreateTaskScreen()) with MaterialPageRoute(builder: (_) =>
ChangeNotifierProvider(create: (_) => CreateTaskProvider(), child:
CreateTaskScreen())) so CreateTaskProvider is in the widget tree before any
context.read()/watch() calls.
In `@src/lib/features/tasks/viewmodel/task_viewmodel.dart`:
- Around line 147-175: fetchTasks() currently builds CategoryModel from
item['category'] string so reloaded tasks lose their real category; change data
retrieval to include the category row (or at least the category_id) and map it
into CategoryModel instead of using a fallback "General". Specifically, modify
the Supabase select to fetch the related category fields (or fetch category by
item['category_id']) and then construct CategoryModel with the real id, name,
colorCode and profileId when creating TaskModel in fetchTasks(), ensuring you
use the actual item['category_id'] and returned category columns rather than
item['category'].
- Around line 209-214: The deleteTask method returns before the task list is
refreshed; update deleteTask (and any call sites expecting completion) to await
the refresh by awaiting fetchTasks() after the delete completes (i.e., change
the call inside deleteTask to await fetchTasks()); keep the try/catch and
existing awaits on Supabase but ensure fetchTasks() is awaited so callers of
deleteTask see the updated list.
- Around line 140-154: fetchTasks currently returns early if
Supabase.auth.currentUser is null but doesn't clear previous state; update the
fetchTasks method to clear _tasks (and call notifyListeners() if this ViewModel
uses ChangeNotifier) before returning when user is null so the UI no longer
shows another user's tasks after logout/session expiry; specifically modify
fetchTasks to check user == null, then call _tasks.clear() (and
notifyListeners() / update any reactive state) and then return.
- Around line 52-72: The method addCustomTag currently calls fire-and-forget
_saveCustomTags(), causing callers to see success even if persistence fails;
change addCustomTag to be asynchronous (Future<String?>) and await
_saveCustomTags(), and make _saveCustomTags return a Future<bool> or throw on
failure; after adding the TagModel to _customTags, await _saveCustomTags(), and
if it fails remove the recently added tag from _customTags, avoid calling
notifyListeners() on failure and return an error message (or rethrow) so callers
see the persistence failure; keep references: addCustomTag, _saveCustomTags,
_customTags, notifyListeners.
- Around line 195-203: updateTask currently updates Supabase but leaves the
in-memory _tasks list stale; after the DB update in updateTask, locate the task
in the _tasks collection by matching its id (taskId), merge/patch the updated
fields from data into that task object (e.g., update title and category_id),
replace or update the entry in _tasks, and then call notifyListeners(); if the
task isn't found, fall back to fetching/refreshing tasks (or insert the patched
task) to keep local state consistent with the server.
---
Outside diff comments:
In `@src/lib/features/tasks/view/screens/create_task_screen.dart`:
- Around line 485-500: The UI renders the same Description input twice using
CustomInputField with the same _descController; remove the duplicate widget
instance so only one Description field remains (keep a single CustomInputField
that uses _descController and its surrounding spacing like the SizedBox), and
verify CreateTaskScreen (or the widget tree containing _descController) no
longer contains the duplicated block to avoid controller reuse and visual
duplication.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 88c67b93-46f0-4ccc-89d7-3e6f0b498fef
📒 Files selected for processing (5)
src/lib/features/tasks/model/task_model.dartsrc/lib/features/tasks/view/screens/create_task_screen.dartsrc/lib/features/tasks/view/widgets/task_widgets.dartsrc/lib/features/tasks/viewmodel/task_viewmodel.dartsrc/lib/main.dart
🚧 Files skipped from review as they are similar to previous changes (2)
- src/lib/main.dart
- src/lib/features/tasks/view/widgets/task_widgets.dart
| Future<void> submitTask( | ||
| BuildContext context, { | ||
| required String taskName, | ||
| required String description, | ||
| required dynamic priority, | ||
| required List<dynamic> tags, | ||
| required int? categoryId, // Thêm tham số ID từ UI truyền vào | ||
| }) async { | ||
| if (taskName.trim().isEmpty) { | ||
| _showSnackBar(context, "Task name is required."); | ||
| return; | ||
| } | ||
|
|
||
| // Check xem có chọn Category chưa | ||
| if (categoryId == null) { | ||
| _showSnackBar(context, "Please select a category."); | ||
| return; | ||
| } | ||
|
|
||
| final user = _supabase.auth.currentUser; | ||
| if (user == null) { | ||
| _showSnackBar(context, "Session not found. Please re-authenticate."); | ||
| return; | ||
| } | ||
|
|
||
| _isLoading = true; | ||
| notifyListeners(); | ||
|
|
||
| try { | ||
| // 1. Xử lý Priority ID | ||
| int priorityId = 3; // Mặc định Medium | ||
| final String priorityStr = priority.toString().toLowerCase(); | ||
|
|
||
| if (priorityStr.contains('urgent')) { | ||
| priorityId = 1; | ||
| } else if (priorityStr.contains('high')) { | ||
| priorityId = 2; | ||
| } else if (priorityStr.contains('medium')) { | ||
| priorityId = 3; | ||
| } else if (priorityStr.contains('low')) { | ||
| priorityId = 4; | ||
| } | ||
|
|
||
| // 2. Xử lý thời gian | ||
| final scheduledDateTime = DateTime( | ||
| _selectedDate.year, | ||
| _selectedDate.month, | ||
| _selectedDate.day, | ||
| _startTime.hour, | ||
| _startTime.minute, | ||
| ); | ||
|
|
||
| // 3. Insert vào Supabase | ||
| await _supabase.from('task').insert({ | ||
| 'title': taskName.trim(), | ||
| //'description': description.trim(), // Tui mở comment cái này ra cho ông luôn | ||
| 'status': 0, | ||
| 'priority': priorityId, | ||
| 'profile_id': user.id, | ||
| 'category_id': categoryId, // Dùng ID thực tế từ UI | ||
| 'create_at': scheduledDateTime.toIso8601String(), | ||
| }); |
There was a problem hiding this comment.
Persist the description and selected tags.
description and tags are accepted from the form, but the insert payload drops them; created tasks lose user-entered details and tag selections.
Proposed fix for description persistence
await _supabase.from('task').insert({
'title': taskName.trim(),
- //'description': description.trim(), // Tui mở comment cái này ra cho ông luôn
+ 'description': description.trim(),
'status': 0,
'priority': priorityId,
'profile_id': user.id,
'category_id': categoryId, // Dùng ID thực tế từ UI
'create_at': scheduledDateTime.toIso8601String(),
});Also persist tags here or remove the parameter until tag persistence is implemented.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| Future<void> submitTask( | |
| BuildContext context, { | |
| required String taskName, | |
| required String description, | |
| required dynamic priority, | |
| required List<dynamic> tags, | |
| required int? categoryId, // Thêm tham số ID từ UI truyền vào | |
| }) async { | |
| if (taskName.trim().isEmpty) { | |
| _showSnackBar(context, "Task name is required."); | |
| return; | |
| } | |
| // Check xem có chọn Category chưa | |
| if (categoryId == null) { | |
| _showSnackBar(context, "Please select a category."); | |
| return; | |
| } | |
| final user = _supabase.auth.currentUser; | |
| if (user == null) { | |
| _showSnackBar(context, "Session not found. Please re-authenticate."); | |
| return; | |
| } | |
| _isLoading = true; | |
| notifyListeners(); | |
| try { | |
| // 1. Xử lý Priority ID | |
| int priorityId = 3; // Mặc định Medium | |
| final String priorityStr = priority.toString().toLowerCase(); | |
| if (priorityStr.contains('urgent')) { | |
| priorityId = 1; | |
| } else if (priorityStr.contains('high')) { | |
| priorityId = 2; | |
| } else if (priorityStr.contains('medium')) { | |
| priorityId = 3; | |
| } else if (priorityStr.contains('low')) { | |
| priorityId = 4; | |
| } | |
| // 2. Xử lý thời gian | |
| final scheduledDateTime = DateTime( | |
| _selectedDate.year, | |
| _selectedDate.month, | |
| _selectedDate.day, | |
| _startTime.hour, | |
| _startTime.minute, | |
| ); | |
| // 3. Insert vào Supabase | |
| await _supabase.from('task').insert({ | |
| 'title': taskName.trim(), | |
| //'description': description.trim(), // Tui mở comment cái này ra cho ông luôn | |
| 'status': 0, | |
| 'priority': priorityId, | |
| 'profile_id': user.id, | |
| 'category_id': categoryId, // Dùng ID thực tế từ UI | |
| 'create_at': scheduledDateTime.toIso8601String(), | |
| }); | |
| Future<void> submitTask( | |
| BuildContext context, { | |
| required String taskName, | |
| required String description, | |
| required dynamic priority, | |
| required List<dynamic> tags, | |
| required int? categoryId, // Thêm tham số ID từ UI truyền vào | |
| }) async { | |
| if (taskName.trim().isEmpty) { | |
| _showSnackBar(context, "Task name is required."); | |
| return; | |
| } | |
| // Check xem có chọn Category chưa | |
| if (categoryId == null) { | |
| _showSnackBar(context, "Please select a category."); | |
| return; | |
| } | |
| final user = _supabase.auth.currentUser; | |
| if (user == null) { | |
| _showSnackBar(context, "Session not found. Please re-authenticate."); | |
| return; | |
| } | |
| _isLoading = true; | |
| notifyListeners(); | |
| try { | |
| // 1. Xử lý Priority ID | |
| int priorityId = 3; // Mặc định Medium | |
| final String priorityStr = priority.toString().toLowerCase(); | |
| if (priorityStr.contains('urgent')) { | |
| priorityId = 1; | |
| } else if (priorityStr.contains('high')) { | |
| priorityId = 2; | |
| } else if (priorityStr.contains('medium')) { | |
| priorityId = 3; | |
| } else if (priorityStr.contains('low')) { | |
| priorityId = 4; | |
| } | |
| // 2. Xử lý thời gian | |
| final scheduledDateTime = DateTime( | |
| _selectedDate.year, | |
| _selectedDate.month, | |
| _selectedDate.day, | |
| _startTime.hour, | |
| _startTime.minute, | |
| ); | |
| // 3. Insert vào Supabase | |
| await _supabase.from('task').insert({ | |
| 'title': taskName.trim(), | |
| 'description': description.trim(), | |
| 'status': 0, | |
| 'priority': priorityId, | |
| 'profile_id': user.id, | |
| 'category_id': categoryId, // Dùng ID thực tế từ UI | |
| 'create_at': scheduledDateTime.toIso8601String(), | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/lib/features/tasks/view/screens/create_task_screen.dart` around lines 66
- 127, The submitTask function drops the description and tags parameters when
inserting into Supabase, so include description in the task insert and persist
tags after the task is created: add 'description': description.trim() to the
payload passed to _supabase.from('task').insert, capture the inserted task's id
from the insert response, then persist tags by inserting into your
join/association (e.g., _supabase.from('task_tag').insert) using that task id
and each tag id or name; if you intend to store tags as a JSON/array column on
the task row instead, write the tags list (e.g., tags.map(...).toList()) into
that column in the same insert. Ensure you handle failures and keep
_isLoading/notifyListeners consistent.
| final categoryViewModel = context.watch<CategoryViewModel>(); | ||
| final tagViewModel = context.watch<TagViewModel>(); | ||
| String formattedDate = DateFormat('EEEE, d MMMM').format(_selectedDate); | ||
| String formattedDate = DateFormat('EEEE, d MMMM').format(context.read<CreateTaskProvider>().selectedDate); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Confirm CreateTaskProvider is registered in the widget tree.
# Expectation: At least one ChangeNotifierProvider/CreateTaskProvider registration exists above CreateTaskScreen.
rg -n -C3 'ChangeNotifierProvider<\s*CreateTaskProvider|Provider<\s*CreateTaskProvider|create:\s*\([^)]*\)\s*=>\s*CreateTaskProvider|MultiProvider'Repository: tqha1011/TaskManagement
Length of output: 394
🏁 Script executed:
# First, search for CreateTaskProvider definition and usages
rg -n 'CreateTaskProvider' --type dartRepository: tqha1011/TaskManagement
Length of output: 1831
🏁 Script executed:
# Get the full MultiProvider setup from main.dart
cat -n src/lib/main.dart | head -100Repository: tqha1011/TaskManagement
Length of output: 3694
🏁 Script executed:
# Check the specific lines in create_task_screen.dart
sed -n '185,200p' src/lib/features/tasks/view/screens/create_task_screen.dartRepository: tqha1011/TaskManagement
Length of output: 643
🏁 Script executed:
# Search for navigation to CreateTaskScreen
rg -n 'CreateTaskScreen|Navigator.push|pushNamed.*create' --type dart -C 2Repository: tqha1011/TaskManagement
Length of output: 6122
🏁 Script executed:
# Look for any route definitions or wrapping of CreateTaskScreen
rg -n 'ChangeNotifierProvider.*CreateTaskProvider|Provider.*CreateTaskProvider' --type dart -B2 -A2Repository: tqha1011/TaskManagement
Length of output: 49
🏁 Script executed:
# Check if there are route definitions or named routes
fd -n 'routes\|app_routes' --type dartRepository: tqha1011/TaskManagement
Length of output: 297
🏁 Script executed:
# Read the CreateTaskProvider definition
sed -n '28,80p' src/lib/features/tasks/view/screens/create_task_screen.dartRepository: tqha1011/TaskManagement
Length of output: 1487
🏁 Script executed:
# Check if CreateTaskProvider is wrapped around the screen elsewhere
rg -n 'ChangeNotifierProvider.*CreateTaskProvider' src/lib/features/tasks/view/screens/ -A 5Repository: tqha1011/TaskManagement
Length of output: 49
Register CreateTaskProvider in the widget tree before accessing it.
CreateTaskScreen reads CreateTaskProvider via context.read() at lines 193, 296–310, and 508–510, but the provider is not registered. The navigation in home_screen.dart (line 83) uses a plain MaterialPageRoute without wrapping CreateTaskProvider, and the root MultiProvider in main.dart does not include it. This will throw ProviderNotFoundException at runtime.
Either add ChangeNotifierProvider<CreateTaskProvider>(create: (_) => CreateTaskProvider()) to the root MultiProvider, or wrap CreateTaskScreen with the provider when navigating.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/lib/features/tasks/view/screens/create_task_screen.dart` at line 193,
CreateTaskScreen is calling context.read<CreateTaskProvider>() (e.g., in
CreateTaskScreen where formattedDate is computed and at other occurrences) but
CreateTaskProvider is not registered, causing ProviderNotFoundException; fix by
registering the provider - either add
ChangeNotifierProvider<CreateTaskProvider>(create: (_) => CreateTaskProvider())
to your app's root MultiProvider so the provider is available app-wide, or wrap
the screen when navigating (replace MaterialPageRoute(builder: (_) =>
CreateTaskScreen()) with MaterialPageRoute(builder: (_) =>
ChangeNotifierProvider(create: (_) => CreateTaskProvider(), child:
CreateTaskScreen())) so CreateTaskProvider is in the widget tree before any
context.read()/watch() calls.
| String? addCustomTag(String name) { | ||
| name = name.trim(); | ||
| if (name.isEmpty) return 'Tên tag không được để trống'; | ||
| if (name.length > _maxCustomTagLength) | ||
| return 'Tối đa $_maxCustomTagLength ký tự'; | ||
| if (_customTags.length >= _maxCustomTags) | ||
| return 'Tối đa $_maxCustomTags tag custom'; | ||
| if (_customTags.any((t) => t.name.toLowerCase() == name.toLowerCase())) { | ||
| return 'Tag đã tồn tại'; | ||
| } | ||
| _customTags.add( | ||
| TagModel( | ||
| id: DateTime.now().millisecondsSinceEpoch, | ||
| name: name, | ||
| colorCode: '#FF9800', | ||
| profileId: '', | ||
| ), | ||
| ); | ||
| _saveCustomTags(); | ||
| notifyListeners(); | ||
| return null; |
There was a problem hiding this comment.
Await custom-tag persistence before reporting success.
_saveCustomTags() is fire-and-forget, so addCustomTag() can return null and notify listeners even if SharedPreferences fails; the tag then disappears on restart.
Proposed fix
- String? addCustomTag(String name) {
+ Future<String?> addCustomTag(String name) async {
name = name.trim();
if (name.isEmpty) return 'Tên tag không được để trống';
if (name.length > _maxCustomTagLength)
return 'Tối đa $_maxCustomTagLength ký tự';
@@
- _saveCustomTags();
+ await _saveCustomTags();
notifyListeners();
return null;
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| String? addCustomTag(String name) { | |
| name = name.trim(); | |
| if (name.isEmpty) return 'Tên tag không được để trống'; | |
| if (name.length > _maxCustomTagLength) | |
| return 'Tối đa $_maxCustomTagLength ký tự'; | |
| if (_customTags.length >= _maxCustomTags) | |
| return 'Tối đa $_maxCustomTags tag custom'; | |
| if (_customTags.any((t) => t.name.toLowerCase() == name.toLowerCase())) { | |
| return 'Tag đã tồn tại'; | |
| } | |
| _customTags.add( | |
| TagModel( | |
| id: DateTime.now().millisecondsSinceEpoch, | |
| name: name, | |
| colorCode: '#FF9800', | |
| profileId: '', | |
| ), | |
| ); | |
| _saveCustomTags(); | |
| notifyListeners(); | |
| return null; | |
| Future<String?> addCustomTag(String name) async { | |
| name = name.trim(); | |
| if (name.isEmpty) return 'Tên tag không được để trống'; | |
| if (name.length > _maxCustomTagLength) | |
| return 'Tối đa $_maxCustomTagLength ký tự'; | |
| if (_customTags.length >= _maxCustomTags) | |
| return 'Tối đa $_maxCustomTags tag custom'; | |
| if (_customTags.any((t) => t.name.toLowerCase() == name.toLowerCase())) { | |
| return 'Tag đã tồn tại'; | |
| } | |
| _customTags.add( | |
| TagModel( | |
| id: DateTime.now().millisecondsSinceEpoch, | |
| name: name, | |
| colorCode: '#FF9800', | |
| profileId: '', | |
| ), | |
| ); | |
| await _saveCustomTags(); | |
| notifyListeners(); | |
| return null; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/lib/features/tasks/viewmodel/task_viewmodel.dart` around lines 52 - 72,
The method addCustomTag currently calls fire-and-forget _saveCustomTags(),
causing callers to see success even if persistence fails; change addCustomTag to
be asynchronous (Future<String?>) and await _saveCustomTags(), and make
_saveCustomTags return a Future<bool> or throw on failure; after adding the
TagModel to _customTags, await _saveCustomTags(), and if it fails remove the
recently added tag from _customTags, avoid calling notifyListeners() on failure
and return an error message (or rethrow) so callers see the persistence failure;
keep references: addCustomTag, _saveCustomTags, _customTags, notifyListeners.
| Future<void> fetchTasks() async { | ||
| final supabase = Supabase.instance.client; | ||
| final user = supabase.auth.currentUser; | ||
|
|
||
| if (user == null) return; | ||
|
|
||
| try { | ||
| final data = await supabase | ||
| .from('task') | ||
| .select('*') | ||
| .eq('profile_id', user.id) | ||
| .order('create_at', ascending: true); | ||
|
|
||
| if (data != null) { | ||
| _tasks.clear(); |
There was a problem hiding this comment.
Clear task state when there is no authenticated user.
Line 144 returns without clearing _tasks, so a logout/session-expiry path can keep showing the previous user’s tasks.
Proposed fix
- if (user == null) return;
+ if (user == null) {
+ _tasks.clear();
+ notifyListeners();
+ return;
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| Future<void> fetchTasks() async { | |
| final supabase = Supabase.instance.client; | |
| final user = supabase.auth.currentUser; | |
| if (user == null) return; | |
| try { | |
| final data = await supabase | |
| .from('task') | |
| .select('*') | |
| .eq('profile_id', user.id) | |
| .order('create_at', ascending: true); | |
| if (data != null) { | |
| _tasks.clear(); | |
| Future<void> fetchTasks() async { | |
| final supabase = Supabase.instance.client; | |
| final user = supabase.auth.currentUser; | |
| if (user == null) { | |
| _tasks.clear(); | |
| notifyListeners(); | |
| return; | |
| } | |
| try { | |
| final data = await supabase | |
| .from('task') | |
| .select('*') | |
| .eq('profile_id', user.id) | |
| .order('create_at', ascending: true); | |
| if (data != null) { | |
| _tasks.clear(); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/lib/features/tasks/viewmodel/task_viewmodel.dart` around lines 140 - 154,
fetchTasks currently returns early if Supabase.auth.currentUser is null but
doesn't clear previous state; update the fetchTasks method to clear _tasks (and
call notifyListeners() if this ViewModel uses ChangeNotifier) before returning
when user is null so the UI no longer shows another user's tasks after
logout/session expiry; specifically modify fetchTasks to check user == null,
then call _tasks.clear() (and notifyListeners() / update any reactive state) and
then return.
| final data = await supabase | ||
| .from('task') | ||
| .select('*') | ||
| .eq('profile_id', user.id) | ||
| .order('create_at', ascending: true); | ||
|
|
||
| if (data != null) { | ||
| _tasks.clear(); | ||
|
|
||
| for (var item in data) { | ||
| // 1. Chuyển đổi Priority trực tiếp | ||
| Priority p = Priority.medium; | ||
| if (item['priority'] == 1) p = Priority.urgent; | ||
| else if (item['priority'] == 2) p = Priority.high; | ||
| else if (item['priority'] == 4) p = Priority.low; | ||
|
|
||
| // 2. Nhét data thẳng vào TaskModel luôn, đách cần fromJson nữa | ||
| _tasks.add(TaskModel( | ||
| id: item['id'].toString(), | ||
| title: item['title'] ?? 'Task mới', | ||
| description: item['description'] ?? '', | ||
|
|
||
| // CHÍNH LÀ CHỖ NÀY: Khởi tạo cục CategoryModel đàng hoàng | ||
| category: CategoryModel( | ||
| id: 0, // Nhét số 0 vào làm ID ảo | ||
| name: item['category']?.toString() ?? 'General', // Lấy tên từ database, nếu rỗng thì cho chữ General | ||
| colorCode: '#5A8DF3', // Lấy màu mặc định | ||
| profileId: '', // Bỏ trống | ||
| ), |
There was a problem hiding this comment.
Preserve the selected category when reloading tasks.
fetchTasks() selects from task and builds the category from item['category'], but task creation stores category_id; after reload, tasks fall back to "General" with fake id 0.
Fetch/join the category row or at least map category_id consistently before creating CategoryModel.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/lib/features/tasks/viewmodel/task_viewmodel.dart` around lines 147 - 175,
fetchTasks() currently builds CategoryModel from item['category'] string so
reloaded tasks lose their real category; change data retrieval to include the
category row (or at least the category_id) and map it into CategoryModel instead
of using a fallback "General". Specifically, modify the Supabase select to fetch
the related category fields (or fetch category by item['category_id']) and then
construct CategoryModel with the real id, name, colorCode and profileId when
creating TaskModel in fetchTasks(), ensuring you use the actual
item['category_id'] and returned category columns rather than item['category'].
| Future<void> updateTask(dynamic taskId, Map<String, dynamic> data) async { | ||
| final _supabase = Supabase.instance.client; | ||
| try { | ||
| await _supabase | ||
| .from('task') | ||
| .update(data) // Data ở đây sẽ chứa {'title': '...', 'category_id': ...} | ||
| .eq('id', taskId); | ||
|
|
||
| notifyListeners(); // Để màn hình Home load lại dữ liệu mới |
There was a problem hiding this comment.
Refresh or patch local state after updating a task.
updateTask() only writes to Supabase and calls notifyListeners(), but _tasks remains unchanged, so listeners rebuild with stale title/category data until another explicit fetch happens.
Proposed fix
await _supabase
.from('task')
.update(data) // Data ở đây sẽ chứa {'title': '...', 'category_id': ...}
.eq('id', taskId);
+ await fetchTasks();
- notifyListeners(); // Để màn hình Home load lại dữ liệu mới🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/lib/features/tasks/viewmodel/task_viewmodel.dart` around lines 195 - 203,
updateTask currently updates Supabase but leaves the in-memory _tasks list
stale; after the DB update in updateTask, locate the task in the _tasks
collection by matching its id (taskId), merge/patch the updated fields from data
into that task object (e.g., update title and category_id), replace or update
the entry in _tasks, and then call notifyListeners(); if the task isn't found,
fall back to fetching/refreshing tasks (or insert the patched task) to keep
local state consistent with the server.
| Future<void> deleteTask(String taskId) async { | ||
| final supabase = Supabase.instance.client; | ||
| try { | ||
| await supabase.from('task').delete().eq('id', taskId); | ||
| // Gọi fetch lại để làm mới danh sách | ||
| fetchTasks(); |
There was a problem hiding this comment.
Await the refresh after deletion.
deleteTask() completes before fetchTasks() finishes, so callers awaiting deletion can still observe the old list.
Proposed fix
await supabase.from('task').delete().eq('id', taskId);
// Gọi fetch lại để làm mới danh sách
- fetchTasks();
+ await fetchTasks();📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| Future<void> deleteTask(String taskId) async { | |
| final supabase = Supabase.instance.client; | |
| try { | |
| await supabase.from('task').delete().eq('id', taskId); | |
| // Gọi fetch lại để làm mới danh sách | |
| fetchTasks(); | |
| Future<void> deleteTask(String taskId) async { | |
| final supabase = Supabase.instance.client; | |
| try { | |
| await supabase.from('task').delete().eq('id', taskId); | |
| // Gọi fetch lại để làm mới danh sách | |
| await fetchTasks(); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/lib/features/tasks/viewmodel/task_viewmodel.dart` around lines 209 - 214,
The deleteTask method returns before the task list is refreshed; update
deleteTask (and any call sites expecting completion) to await the refresh by
awaiting fetchTasks() after the delete completes (i.e., change the call inside
deleteTask to await fetchTasks()); keep the try/catch and existing awaits on
Supabase but ensure fetchTasks() is awaited so callers of deleteTask see the
updated list.
Summary by CodeRabbit
Release Notes
New Features
Improvements